# -*- coding: utf-8 -*-

# Standard library imports
from functools import wraps
from contextlib import contextmanager

# py2neo imports
from py2neo import Graph
from py2neo import Node
from py2neo import Relationship
from py2neo import NodeMatcher
from py2neo import RelationshipMatcher


def require_transaction(func):
    """
    这是一个装饰器。用途: 涉及neo4j数据增删改的函数，在运行前检查数据库事务管理器是否开启，如果没有开启，则抛出异常。
    """
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        if not self._transaction:  # 检查当前事务状态
            raise ConnectionError("代码中必须先开启neo4j事务才能进行数据增删改操作，with neo4j_client.transaction() as tx:")
        return func(self, *args, **kwargs)
    return wrapper

class Neo4jClient:
    """
    py2neo库有graph数据库对象和graph.begin()事务管理器。
    graph对象的操作是自动提交的，不需要commit()。但是graph对象的操作如果出错不可以回滚，具有破坏性。主要用于查询操作和清库操作。
    如果涉及到数据的增删改操作，则需要使用事务管理器。
    graph.begin()返回一个事务管理器对象，可以用with语句进行上下文管理。
    示例：
    neo4j_client = Neo4jClient(uri="bolt://localhost:7687", auth=("neo4j", "password"))
    graph = neo4j_client.graph
    graph.delete_all()
    with neo4j_client.transaction():  # 打开就行，不用 as tx
        neo4j_client.create_node("Person", {"name": "Alice", "age": 30})
    """
    def __init__(self, uri: str, username: str, password: str):
        self.graph = Graph(uri, auth=(username, password))
        self.node_matcher = NodeMatcher(self.graph)
        self.relationship_matcher = RelationshipMatcher(self.graph)
        self._transaction = None  # 当前事务对象

    @contextmanager
    def transaction(self):
        """事务上下文管理器"""
        try:
            self._transaction = self.graph.begin()
            yield self
            self._transaction.commit()
        except Exception as e:
            if self._transaction:
                self._transaction.rollback()
            raise e
        finally:
            self._transaction = None

    def execute_cypher(self, cypher: str, **parameters):
        """执行Cypher语句（自动判断模式）"""
        if self._transaction:
            # 事务模式下使用cursor执行
            return self._transaction.run(cypher, parameters)
        else:
            # 直接提交模式
            return self.graph.run(cypher, parameters)

    def delete_all(self):
        """清空数据库（强制使用自动提交）"""
        self.graph.delete_all()

    # 传入标签名（表名）和节点的属性字典，创建节点并返回
    @require_transaction
    def create_node(self, label, properties):
        node = Node(label, **properties)
        self._transaction.create(node)
        return node

    @require_transaction
    def create_relationship(self, start_label,start_node_properties, end_label, end_node_properties, relation_type, relation_properties: dict):
        start_nodes = self.query_nodes_by_cypher(start_label, start_node_properties)
        if not start_nodes:
            raise ValueError(f"start_node not found,properties: {start_node_properties},label: {start_label}")
        if len(start_nodes) > 1:
            raise ValueError(f"more than one start_node found,properties: {start_node_properties},label: {start_label}")
        start_node = start_nodes[0]
        end_nodes = self.query_nodes_by_cypher(end_label, end_node_properties)
        if not end_nodes:
            raise ValueError(f"end_node not found,id_value: {end_node_properties},label: {end_label}")
        if len(end_nodes) > 1:
            raise ValueError(f"more than one end_node found,id_value: {end_node_properties},label: {end_label}")
        end_node = end_nodes[0]
        relationship = Relationship(start_node, relation_type, end_node, **relation_properties)
        self._transaction.create(relationship)
        return relationship

    # 根据id属性，查询符合条件的所有节点
    def query_nodes_by_id(self, label: str, id_name: str, id_value: str):
        """
        查询满足指定标签和属性条件的节点集合
        """
        return self.node_matcher.match(label, **{id_name: id_value}).all()

    # 根据属性字典，查询符合条件的所有节点
    def query_nodes_by_cypher(self, label: str, properties_dict: dict)->list:
        """
        返回：一个列表，所有元素都是节点对象，类型是<class 'py2neo.data.Node'>
        函数功能：查询满足指定标签和属性条件的节点集合
        查询有两种方式，一种是使用match函数，另一种是使用cypher语句。
        match函数要求数值类型完全一致，而cypher语句可以忽略整形和浮点型的精度，所以推荐使用cyper语句
        """
        # 遍历properties_dict,如果值是字符串，在两侧加上单引号，如果值可以转化为int或者float，则不动，不加单引号。
        for k,v in properties_dict.items():
            is_str = False
            if str(v).isdigit():
                continue
            try:
                float(v)
                continue
            except ValueError:
                is_str = True
            if is_str:
                properties_dict[k] = f"'{v}'"
        # 使用match函数查询方法（不推荐）
        # nodes = self.node_matcher.match(label, **properties).all()
        # 使用cypher语句查询
        where_clause = " AND ".join([f"n.{k}={v}" for k, v in properties_dict.items()])
        query = f"MATCH (n:{label}) WHERE {where_clause} RETURN n"
        nodes = self.graph.run(query).data()
        # 这个nodes类型是list，每个元素是dict，dict的key是字符串n，value是节点对象，类型是<class 'py2neo.data.Node'>
        if not nodes:
            return []
        else:
            result = [n["n"] for n in nodes]
            return result

    # 根据node对象，查询对象的所有关系
    def query_relationships(self, node):
        """
        在neo4j数据库中，关系是具有方向性的，通过节点查询到的关系，是指从该节点出发，指向其他节点的关系（出度关系）。
        """
        relations = self.relationship_matcher.match(nodes=[node])
        # print("type(relations))--------",type(relations))
        return relations

    # 根据node对象，查询对象的所有属性
    def query_properties(self, node):
        return dict(node)

    # 根据关系类型，查询关系的所有起始节点和终止节点
    def query_relationship_nodes(self, relation_type: str)->list:
        relations = self.relationship_matcher.match(r_type=relation_type)
        node_list = [[rel.start_node, rel.end_node] for rel in relations]
        return node_list



